blob: fda250b248286054e4f324548e23845515536431 [file] [log] [blame]
/*
Copyright 2016 The Transicator Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package snapshotserver
import (
"database/sql"
"database/sql/driver"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/http/httputil"
"net/url"
"os"
"path"
"time"
"github.com/apigee-labs/transicator/common"
"github.com/julienschmidt/httprouter"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/pkg/errors"
)
const roundedTimestampFormat = "2006-01-02 15:04:05.999999-07:00"
var _ = Describe("Snapshot API Tests", func() {
It("Default snapshot", func() {
insertApp("jsonSnap", "jsonSnap", "snaptests")
for _, p := range []string{"scope", "selector"} {
func() {
resp, err := http.Get(fmt.Sprintf("%s/snapshots?%s=snaptests", testBase, p))
Expect(err).Should(Succeed())
defer resp.Body.Close()
Expect(resp.StatusCode).Should(Equal(200))
Expect(resp.Header.Get("Content-Type")).Should(Equal("application/json"))
bod, err := ioutil.ReadAll(resp.Body)
Expect(err).Should(Succeed())
ss, err := common.UnmarshalSnapshot(bod)
Expect(err).Should(Succeed())
appTable := getTable(ss, "public.app")
r := getRowByID(appTable, "jsonSnap")
Expect(r).ShouldNot(BeNil())
var org string
err = r.Get("org", &org)
Expect(err).Should(Succeed())
Expect(org).Should(Equal("testorg"))
var appID string
err = r.Get("id", &appID)
Expect(err).Should(Succeed())
Expect(appID).Should(Equal("jsonSnap"))
var devID string
err = r.Get("dev_id", &devID)
Expect(err).Should(Succeed())
Expect(devID).Should(Equal("jsonSnap"))
devTable := getTable(ss, "public.developer")
r = getRowByID(devTable, "jsonSnap")
err = r.Get("org", &org)
Expect(err).Should(Succeed())
Expect(org).Should(Equal("testorg"))
err = r.Get("id", &devID)
Expect(err).Should(Succeed())
Expect(devID).Should(Equal("jsonSnap"))
}()
}
})
It("JSON snapshot two scopes", func() {
insertApp("jsonSnap2", "jsonSnap2", "snaptests2")
insertApp("jsonSnap3", "jsonSnap3", "snaptests2")
for _, p := range []string{"scope", "selector"} {
func() {
u := fmt.Sprintf("%s/snapshots?%s=snaptests2&%s=snaptests3", testBase, p, p)
req := createStandardRequest("GET", u, "application/json", nil)
resp, err := http.DefaultClient.Do(req)
Expect(err).Should(Succeed())
defer resp.Body.Close()
Expect(resp.StatusCode).Should(Equal(200))
Expect(resp.Header.Get("Content-Type")).Should(Equal("application/json"))
bod, err := ioutil.ReadAll(resp.Body)
Expect(err).Should(Succeed())
ss, err := common.UnmarshalSnapshot(bod)
Expect(err).Should(Succeed())
appTable := getTable(ss, "public.app")
r := getRowByID(appTable, "jsonSnap2")
Expect(r).ShouldNot(BeNil())
r2 := getRowByID(appTable, "jsonSnap3")
Expect(r2).ShouldNot(BeNil())
var org string
err = r.Get("org", &org)
Expect(err).Should(Succeed())
Expect(org).Should(Equal("testorg"))
var appID string
err = r.Get("id", &appID)
Expect(err).Should(Succeed())
Expect(appID).Should(Equal("jsonSnap2"))
var devID string
err = r.Get("dev_id", &devID)
Expect(err).Should(Succeed())
Expect(devID).Should(Equal("jsonSnap2"))
devTable := getTable(ss, "public.developer")
r = getRowByID(devTable, "jsonSnap2")
err = r.Get("org", &org)
Expect(err).Should(Succeed())
Expect(org).Should(Equal("testorg"))
err = r.Get("id", &devID)
Expect(err).Should(Succeed())
Expect(devID).Should(Equal("jsonSnap2"))
}()
}
})
It("should return error if DB error", func() {
router := httprouter.New()
router.GET("/snapshots", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
defer GinkgoRecover()
GenSnapshot(w, r)
})
sql.Register("badDriver", badDriver{})
router.GET("/data", func(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
defer GinkgoRecover()
db, err := sql.Open("badDriver", "badDriver")
Expect(err).NotTo(HaveOccurred())
DownloadSnapshot(w, r, db, p)
})
ts := httptest.NewServer(router)
for _, p := range []string{"scope", "selector"} {
func() {
resp, err := http.Get(fmt.Sprintf("%s/snapshots?%s=snaptests", ts.URL, p))
Expect(err).NotTo(HaveOccurred())
defer resp.Body.Close()
Expect(resp.StatusCode).Should(Equal(500))
}()
}
})
It("SQLite snapshot", func() {
dbDir, err := ioutil.TempDir("", "snaptest")
Expect(err).Should(Succeed())
defer os.RemoveAll(dbDir)
insertApp("sqlSnap", "sqlSnap", "snaptests3")
for _, p := range []string{"scope", "selector"} {
func() {
u := fmt.Sprintf("%s/snapshots?%s=snaptests3", testBase, p)
req := createStandardRequest("GET", u, "application/transicator+sqlite", nil)
resp, err := http.DefaultClient.Do(req)
Expect(err).Should(Succeed())
defer resp.Body.Close()
Expect(resp.StatusCode).Should(Equal(200))
Expect(resp.Header.Get("Content-Type")).Should(Equal("application/transicator+sqlite"))
dbFileName := path.Join(dbDir, "snapdb")
outFile, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666)
Expect(err).Should(Succeed())
_, err = io.Copy(outFile, resp.Body)
outFile.Close()
err, cksum := generateEtag(dbFileName)
Expect(err).Should(Succeed())
Expect(resp.Header.Get("ETag")).To(Equal(cksum))
Expect(err).Should(Succeed())
sdb, err := sql.Open("sqlite3", dbFileName)
Expect(err).Should(Succeed())
defer sdb.Close()
row := sdb.QueryRow("select org, id, dev_id from public_app where id = 'sqlSnap'")
var org string
var id string
var devID string
err = row.Scan(&org, &id, &devID)
Expect(err).Should(Succeed())
Expect(org).Should(Equal("testorg"))
Expect(id).Should(Equal("sqlSnap"))
Expect(devID).Should(Equal("sqlSnap"))
rows, err := sdb.Query(`
select columnName, primaryKey from _transicator_tables
where tableName = 'public_app'
`)
Expect(err).Should(Succeed())
defer rows.Close()
r := make(map[string]int)
for rows.Next() {
var cn string
var pk bool
err = rows.Scan(&cn, &pk)
Expect(err).Should(Succeed())
if pk {
r[cn] = 2
} else {
r[cn] = 1
}
}
Expect(r["org"]).Should(Equal(1))
Expect(r["dev_id"]).Should(Equal(1))
Expect(r["display_name"]).Should(Equal(1))
Expect(r["name"]).Should(Equal(1))
Expect(r["created_at"]).Should(Equal(1))
Expect(r["created_by"]).Should(Equal(1))
Expect(r["id"]).Should(Equal(2))
Expect(r["_change_selector"]).Should(Equal(2))
row = sdb.QueryRow(
"select value from _transicator_metadata where key = 'snapshot'")
var snap string
err = row.Scan(&snap)
Expect(err).Should(Succeed())
Expect(resp.Header.Get("Transicator-Snapshot-TXID")).To(Equal(snap))
}()
}
})
It("SQLite snapshot empty scope", func() {
dbDir, err := ioutil.TempDir("", "snaptest")
Expect(err).Should(Succeed())
defer os.RemoveAll(dbDir)
for _, p := range []string{"scope", "selector"} {
func() {
u := fmt.Sprintf("%s/snapshots?%s=wrong_scope_you_dope", testBase, p)
req := createStandardRequest("GET", u, "application/transicator+sqlite", nil)
resp, err := http.DefaultClient.Do(req)
Expect(err).Should(Succeed())
defer resp.Body.Close()
Expect(resp.StatusCode).Should(Equal(200))
Expect(resp.Header.Get("Content-Type")).Should(Equal("application/transicator+sqlite"))
dbFileName := path.Join(dbDir, "snapdb")
outFile, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666)
Expect(err).Should(Succeed())
_, err = io.Copy(outFile, resp.Body)
outFile.Close()
Expect(err).Should(Succeed())
err, cksum := generateEtag(dbFileName)
Expect(err).Should(Succeed())
Expect(resp.Header.Get("ETag")).To(Equal(cksum))
sdb, err := sql.Open("sqlite3", dbFileName)
Expect(err).Should(Succeed())
defer sdb.Close()
row := sdb.QueryRow("select count(*) from public_app")
var count int
err = row.Scan(&count)
Expect(err).Should(Succeed())
Expect(count).Should(BeZero())
}()
}
})
It("SQLite snapshot missing scope", func() {
u := fmt.Sprintf("%s/snapshots", testBase)
req := createStandardRequest("GET", u, "application/json", nil)
resp, err := http.DefaultClient.Do(req)
Expect(err).Should(Succeed())
defer resp.Body.Close()
checkApiErrorCode(resp, http.StatusBadRequest, "MISSING_SCOPE")
})
It("SQLite snapshot bad accept header", func() {
for _, p := range []string{"scope", "selector"} {
func() {
u := fmt.Sprintf("%s/snapshots?%s=nope_not_the_scope", testBase, p)
req := createStandardRequest("GET", u, "text/plain", nil)
resp, err := http.DefaultClient.Do(req)
Expect(err).Should(Succeed())
defer resp.Body.Close()
checkApiErrorCode(resp, http.StatusUnsupportedMediaType, "UNSUPPORTED_MEDIA_TYPE")
}()
}
})
It("SQLite types", func() {
is, err := db.Prepare(`
insert into public.snapshot_test
(id, bool, smallint, bigint, float, double, date, time, timestamp, timestampp, blob, _change_selector)
values
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
`)
Expect(err).Should(Succeed())
testTime := time.Now()
testTimestampStr := testTime.UTC().Format(roundedTimestampFormat)
testDateStr := testTime.Format("2006-01-02")
testTimeStr := testTime.Format("15:04:05-07")
testBuf := []byte("Hello, World!")
_, err = is.Exec("types1", true, int16(123), int64(456), float32(1.23), float64(4.56),
testTime, testTime, testTime, testTime, testBuf, "typetest")
Expect(err).Should(Succeed())
_, err = is.Exec("types2", true, int16(-123), int64(-456), float32(-1.23), float64(-4.56),
testTime, testTime, testTime, testTime, testBuf, "typetest")
Expect(err).Should(Succeed())
_, err = is.Exec("types3", false, 0, 0, 0.0, 0.0, nil, nil, nil, nil, nil, "typetest")
Expect(err).Should(Succeed())
dbDir, err := ioutil.TempDir("", "snaptest3")
Expect(err).Should(Succeed())
defer os.RemoveAll(dbDir)
sdb, _ := getSqliteSnapshot(dbDir, "typetest")
defer sdb.Close()
query := `
select bool, smallint, bigint, float, double, date, time, timestamp, timestampp, blob
from public_snapshot_test
where _change_selector = 'typetest'
order by id
`
rows, err := sdb.Query(query)
Expect(err).Should(Succeed())
defer rows.Close()
var b bool
var si int16
var sl int64
var fs float32
var fl float64
var da string
var tim string
var ts string
var tss string
var bb []byte
Expect(rows.Next()).Should(BeTrue())
err = rows.Scan(&b, &si, &sl, &fs, &fl, &da, &tim, &ts, &tss, &bb)
Expect(err).Should(Succeed())
Expect(b).Should(BeTrue())
Expect(si).Should(Equal(int16(123)))
Expect(sl).Should(Equal(int64(456)))
Expect(fs).Should(Equal(float32(1.23)))
Expect(fl).Should(Equal(float64(4.56)))
Expect(da).Should(Equal(testDateStr))
Expect(tim).Should(Equal(testTimeStr))
Expect(ts).Should(Equal(testTimestampStr))
Expect(tss).Should(Equal(testTimestampStr))
Expect(bb).Should(Equal(testBuf))
Expect(rows.Next()).Should(BeTrue())
err = rows.Scan(&b, &si, &sl, &fs, &fl, &da, &tim, &ts, &tss, &bb)
Expect(err).Should(Succeed())
Expect(b).Should(BeTrue())
Expect(si).Should(Equal(int16(-123)))
Expect(sl).Should(Equal(int64(-456)))
Expect(fs).Should(Equal(float32(-1.23)))
Expect(fl).Should(Equal(float64(-4.56)))
Expect(da).Should(Equal(testDateStr))
Expect(tim).Should(Equal(testTimeStr))
Expect(ts).Should(Equal(testTimestampStr))
Expect(tss).Should(Equal(testTimestampStr))
Expect(bb).Should(Equal(testBuf))
Expect(rows.Next()).Should(BeTrue())
err = rows.Scan(&b, &si, &sl, &fs, &fl, &da, &tim, &ts, &tss, &bb)
Expect(err).Should(Succeed())
Expect(b).Should(BeFalse())
Expect(si).Should(BeZero())
Expect(sl).Should(BeZero())
Expect(fs).Should(BeZero())
Expect(fl).Should(BeZero())
Expect(da).Should(BeEmpty())
Expect(tim).Should(BeEmpty())
Expect(ts).Should(BeEmpty())
Expect(tss).Should(BeEmpty())
Expect(bb).Should(BeNil())
Expect(rows.Next()).ShouldNot(BeTrue())
})
It("should detect invalid chars in scope query param", func() {
for _, p := range []string{"scope", "selector"} {
func() {
u := fmt.Sprintf("%s/snapshots?%s=%s", testBase, p, url.QueryEscape("snaptests;select now()"))
req := createStandardRequest("GET", u, "application/json", nil)
resp, err := http.DefaultClient.Do(req)
Expect(err).Should(Succeed())
defer resp.Body.Close()
checkApiErrorCode(resp, http.StatusBadRequest, "INVALID_REQUEST_PARAM")
}()
}
})
It("should detect invalid chars in multiple scope query params", func() {
for _, p := range []string{"scope", "selector"} {
func() {
u := fmt.Sprintf("%s/snapshots?%s=abc123&%s=%s", testBase, p, p, url.QueryEscape("snapstests;select now()"))
req := createStandardRequest("GET", u, "application/json", nil)
resp, err := http.DefaultClient.Do(req)
Expect(err).Should(Succeed())
defer resp.Body.Close()
checkApiErrorCode(resp, http.StatusBadRequest, "INVALID_REQUEST_PARAM")
}()
}
})
})
func createStandardRequest(method string, urlStr string, acceptHdr string, body io.Reader) *http.Request {
req, err := http.NewRequest(method, urlStr, body)
Expect(err).Should(Succeed())
req.Header = http.Header{}
req.Header.Set("Accept", acceptHdr)
dump, err := httputil.DumpRequestOut(req, true)
fmt.Fprintf(GinkgoWriter, "\ndump client req: %q\nerr: %+v\n", dump, err)
return req
}
func checkApiErrorCode(resp *http.Response, sc int, ec string) {
dump, err := httputil.DumpResponse(resp, true)
fmt.Fprintf(GinkgoWriter, "\nAPIError response: %q\nerr: %+v\n", dump, err)
Expect(resp.StatusCode).Should(Equal(sc))
Expect(resp.Header.Get("Content-Type")).Should(Equal("application/json"))
bod, err := ioutil.ReadAll(resp.Body)
Expect(err).Should(Succeed())
var errMsg common.APIError
err = json.Unmarshal(bod, &errMsg)
Expect(err).Should(Succeed())
Expect(errMsg.Code).Should(Equal(ec))
}
func insertApp(devID, appID, selector string) {
_, err := db.Exec(`
insert into public.developer (org, id, created_at, created_by, _change_selector)
values ('testorg', $1, now(), 'testcreator', $2)
`, devID, selector)
Expect(err).Should(Succeed())
_, err = db.Exec(`
insert into public.app (org, id, dev_id, created_at, created_by, _change_selector)
values ('testorg', $1, $2, now(), 'testcreator', $3)
`, appID, devID, selector)
Expect(err).Should(Succeed())
}
func getTable(ss *common.Snapshot, name string) *common.Table {
for _, t := range ss.Tables {
if t.Name == name {
return &t
}
}
Expect(false).Should(BeTrue())
return nil
}
func getRowByID(t *common.Table, id string) common.Row {
for _, r := range t.Rows {
if r["id"] != nil && r["id"].Value.(string) == id {
return r
}
}
Expect(false).Should(BeTrue())
return nil
}
func getSqliteSnapshot(dbDir, scope string) (*sql.DB, string) {
u := fmt.Sprintf("%s/snapshots?scope=%s", testBase, scope)
req := createStandardRequest("GET", u, "application/transicator+sqlite", nil)
resp, err := http.DefaultClient.Do(req)
Expect(err).Should(Succeed())
defer resp.Body.Close()
if resp.StatusCode != 200 {
var buf []byte
buf, err = ioutil.ReadAll(resp.Body)
Expect(err).Should(Succeed())
fmt.Fprintf(GinkgoWriter, "Error on response: %s\n", string(buf))
}
Expect(resp.StatusCode).Should(Equal(200))
Expect(resp.Header.Get("Content-Type")).Should(Equal("application/transicator+sqlite"))
dbFileName := path.Join(dbDir, "snapdb")
outFile, err := os.OpenFile(dbFileName, os.O_RDWR|os.O_CREATE, 0666)
Expect(err).Should(Succeed())
_, err = io.Copy(outFile, resp.Body)
outFile.Close()
Expect(err).Should(Succeed())
err, cksum := generateEtag(dbFileName)
Expect(err).Should(Succeed())
Expect(resp.Header.Get("ETag")).To(Equal(cksum))
Expect(err).Should(Succeed())
sdb, err := sql.Open("sqlite3", dbFileName)
Expect(err).Should(Succeed())
row := sdb.QueryRow(
"select value from _transicator_metadata where key = 'snapshot'")
var snap string
err = row.Scan(&snap)
Expect(err).Should(Succeed())
return sdb, snap
}
type badDriver struct{}
func (d badDriver) Open(name string) (driver.Conn, error) {
return nil, errors.New("intentional failure for testing")
}