metabase_smoketest/
metabase-smoketest.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use std::time::Duration;
11
12use anyhow::{Context, bail};
13use itertools::Itertools;
14use tokio::net::TcpStream;
15use tokio_postgres::NoTls;
16use tracing::debug;
17
18use mz_metabase::{
19    DatabaseMetadata, LoginRequest, SetupDatabase, SetupDatabaseDetails, SetupPrefs, SetupRequest,
20    SetupUser, Table, TableField,
21};
22use mz_ore::retry::Retry;
23use mz_ore::task;
24
25const DUMMY_EMAIL: &str = "ci@materialize.io";
26const DUMMY_PASSWORD: &str = "dummydummy1";
27
28async fn connect_materialized() -> Result<tokio_postgres::Client, anyhow::Error> {
29    Retry::default()
30        .retry_async(|_| async {
31            let res = TcpStream::connect("materialized:6875").await;
32            if let Err(e) = &res {
33                debug!("error connecting to materialized: {}", e);
34            }
35            res
36        })
37        .await?;
38    let (client, conn) = tokio_postgres::connect(
39        "postgres://materialize@materialized:6875/materialize",
40        NoTls,
41    )
42    .await
43    .context("failed connecting to materialized")?;
44    task::spawn(|| "metabase_smoketest_mz", async {
45        if let Err(e) = conn.await {
46            panic!("postgres connection error: {}", e);
47        }
48    });
49    Ok(client)
50}
51
52async fn connect_metabase() -> Result<mz_metabase::Client, anyhow::Error> {
53    let mut client = mz_metabase::Client::new("http://metabase:3000")
54        .context("failed creating metabase client")?;
55    let setup_token = Retry::default()
56        .max_duration(Duration::from_secs(30))
57        .retry_async(|_| async {
58            let res = client.session_properties().await;
59            if let Err(e) = &res {
60                debug!("error connecting to metabase: {}", e);
61            }
62            res.map(|res| res.setup_token)
63        })
64        .await?;
65    let session_id = match setup_token {
66        None => {
67            let req = LoginRequest {
68                username: DUMMY_EMAIL.into(),
69                password: DUMMY_PASSWORD.into(),
70            };
71            client.login(&req).await?.id
72        }
73        Some(setup_token) => {
74            let req = &SetupRequest {
75                allow_tracking: false,
76                database: SetupDatabase {
77                    engine: "postgres".into(),
78                    name: "Materialize".into(),
79                    details: SetupDatabaseDetails {
80                        host: "materialized".into(),
81                        port: 6875,
82                        dbname: "materialize".into(),
83                        user: "materialize".into(),
84                    },
85                },
86                token: setup_token,
87                prefs: SetupPrefs {
88                    site_name: "Materialize".into(),
89                },
90                user: SetupUser {
91                    email: DUMMY_EMAIL.into(),
92                    first_name: "Materialize".into(),
93                    last_name: "CI".into(),
94                    password: DUMMY_PASSWORD.into(),
95                    site_name: "Materialize".into(),
96                },
97            };
98            client.setup(req).await?.id
99        }
100    };
101    client.set_session_id(session_id);
102    Ok(client)
103}
104
105#[tokio::main]
106async fn main() -> Result<(), anyhow::Error> {
107    mz_ore::test::init_logging();
108
109    let pgclient = connect_materialized().await?;
110    pgclient
111        .batch_execute(
112            "CREATE OR REPLACE MATERIALIZED VIEW orders (id, date, quantity, total) AS
113             VALUES (1, '2020-01-03'::date, 6, 10.99), (2, '2020-01-04'::date, 4, 7.48)",
114        )
115        .await?;
116
117    let metabase_client = connect_metabase().await?;
118
119    let databases = metabase_client.databases().await?;
120    debug!("Databases: {:#?}", databases);
121
122    let database_names: Vec<_> = databases.iter().map(|d| &d.name).sorted().collect();
123    assert_eq!(database_names, &["Materialize", "Sample Dataset"]);
124
125    let mzdb = databases.iter().find(|d| d.name == "Materialize").unwrap();
126    let expected_metadata = DatabaseMetadata {
127        tables: vec![Table {
128            name: "orders".into(),
129            schema: "public".into(),
130            fields: vec![
131                TableField {
132                    name: "date".into(),
133                    database_type: "date".into(),
134                    base_type: "type/Date".into(),
135                    special_type: None,
136                },
137                TableField {
138                    name: "id".into(),
139                    database_type: "int4".into(),
140                    base_type: "type/Integer".into(),
141                    special_type: None,
142                },
143                TableField {
144                    name: "quantity".into(),
145                    database_type: "int4".into(),
146                    base_type: "type/Integer".into(),
147                    special_type: None,
148                },
149                TableField {
150                    name: "total".into(),
151                    database_type: "numeric".into(),
152                    base_type: "type/Decimal".into(),
153                    special_type: None,
154                },
155            ],
156        }],
157    };
158    // The database sync happens asynchronously and the API doesn't appear to
159    // expose when it is complete, so just retry a few times waiting for the
160    // metadata we expect.
161    Retry::default()
162        .retry_async(|_| async {
163            let mut metadata = metabase_client.database_metadata(mzdb.id).await?;
164            metadata.tables.retain(|t| t.schema == "public");
165            metadata.tables.sort_by(|a, b| a.name.cmp(&b.name));
166            for t in &mut metadata.tables {
167                t.fields.sort_by(|a, b| a.name.cmp(&b.name));
168            }
169            debug!("Materialize database metadata: {:#?}", metadata);
170            if expected_metadata != metadata {
171                bail!(
172                    "metadata did not match\nexpected:\n{:#?}\nactual:\n{:#?}",
173                    expected_metadata,
174                    metadata,
175                );
176            }
177            Ok(())
178        })
179        .await?;
180
181    println!("OK");
182    Ok(())
183}