mirror of
https://github.com/bringout/oca-ocb-core.git
synced 2026-04-20 03:32:00 +02:00
17.0 vanilla
This commit is contained in:
parent
df627a6bba
commit
d72e748793
66 changed files with 116028 additions and 0 deletions
|
|
@ -0,0 +1,18 @@
|
|||
<root>
|
||||
<table id="cool-table" class="fancy-style">
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>1</td>
|
||||
<td>1</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table id="cool-table" class="fancy-style">
|
||||
<tr>
|
||||
<td>
|
||||
<td>2</td>
|
||||
<td>2</td>
|
||||
<td>2</td>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</root>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<root>
|
||||
<table id="cool-table" class="fancy-style">
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>1</td>
|
||||
<td>1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<td>2</td>
|
||||
<td>2</td>
|
||||
<td>2</td>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</root>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<root>
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<table>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>4</td>
|
||||
<td>4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td>5</td>
|
||||
<td>5</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table>
|
||||
<tr>
|
||||
<td>6</td>
|
||||
<td>6</td>
|
||||
<td>6</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td>7</td>
|
||||
<td>7</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>1</td>
|
||||
<td>1</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>2</td>
|
||||
<td>2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>3</td>
|
||||
<td>3</td>
|
||||
</tr>
|
||||
</table>
|
||||
</root>
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<root>
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<table>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>4</td>
|
||||
<td>4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td>5</td>
|
||||
<td>5</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>6</td>
|
||||
<td>6</td>
|
||||
<td>6</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td>7</td>
|
||||
<td>7</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>1</td>
|
||||
<td>1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>2</td>
|
||||
<td>2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>3</td>
|
||||
<td>3</td>
|
||||
</tr>
|
||||
</table>
|
||||
</root>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<root>
|
||||
<table>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>1</td>
|
||||
<td>1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>2</td>
|
||||
<td>2</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>3</td>
|
||||
<td>3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>4</td>
|
||||
<td>4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td>5</td>
|
||||
<td>5</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table>
|
||||
<tr>
|
||||
<td>6</td>
|
||||
<td>6</td>
|
||||
<td>6</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td>7</td>
|
||||
<td>7</td>
|
||||
</tr>
|
||||
</table>
|
||||
</root>
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<root>
|
||||
<table>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>1</td>
|
||||
<td>1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>2</td>
|
||||
<td>2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>3</td>
|
||||
<td>3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>4</td>
|
||||
<td>4</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td>5</td>
|
||||
<td>5</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>6</td>
|
||||
<td>6</td>
|
||||
<td>6</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
<td>7</td>
|
||||
<td>7</td>
|
||||
</tr>
|
||||
</table>
|
||||
</root>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<root>
|
||||
<table>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>1</td>
|
||||
<td>1</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>2</td>
|
||||
<td>2</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>3</td>
|
||||
<td>3</td>
|
||||
</tr>
|
||||
</table>
|
||||
</root>
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
<root>
|
||||
<table>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>1</td>
|
||||
<td>1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>2</td>
|
||||
<td>2</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>3</td>
|
||||
<td>3</td>
|
||||
</tr>
|
||||
</table>
|
||||
</root>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<root>
|
||||
<table>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>1</td>
|
||||
<td>1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>2</td>
|
||||
<td>2</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>3</td>
|
||||
<td>3</td>
|
||||
</tr>
|
||||
</table>
|
||||
</root>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIByTCCAXugAwIBAgIUXOt3OuI/y68E6ogXPRKjMxM3d4kwBQYDK2VwMFExCzAJ
|
||||
BgNVBAYTAkJFMRQwEgYDVQQHDAtIb3V0ZXNpcGxvdTEsMCoGA1UEAwwjSG91dGVz
|
||||
aXBsb3UgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwIBcNMjQwNDIyMTUxNDU3WhgP
|
||||
MzAyMzA4MjQxNTE0NTdaMFExCzAJBgNVBAYTAkJFMRQwEgYDVQQHDAtIb3V0ZXNp
|
||||
cGxvdTEsMCoGA1UEAwwjSG91dGVzaXBsb3UgQ2VydGlmaWNhdGlvbiBBdXRob3Jp
|
||||
dHkwKjAFBgMrZXADIQA9E6L8dKzknznGYYkNMVRt78Rs8L5sIOLFaJHfzfpcdaNj
|
||||
MGEwHQYDVR0OBBYEFOCOFRwpR4cKRG3au4pqU5tc8ItOMB8GA1UdIwQYMBaAFOCO
|
||||
FRwpR4cKRG3au4pqU5tc8ItOMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD
|
||||
AgGGMAUGAytlcANBAMDS7x9X9IIM1jCQdzTibtVu14mZmXoBJ8GoFMojUSvyz86y
|
||||
81vpi1ZEOZoZm/HadUBlONvL4aGqbu0t6yABeA8=
|
||||
-----END CERTIFICATE-----
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIICCTCCAbugAwIBAgICEAAwBQYDK2VwMFExCzAJBgNVBAYTAkJFMRQwEgYDVQQH
|
||||
DAtIb3V0ZXNpcGxvdTEsMCoGA1UEAwwjSG91dGVzaXBsb3UgQ2VydGlmaWNhdGlv
|
||||
biBBdXRob3JpdHkwIBcNMjQwNDIyMTUxNDU3WhgPMzAyMzA4MjQxNTE0NTdaMEAx
|
||||
CzAJBgNVBAYTAkJFMRQwEgYDVQQHDAtIb3V0ZXNpcGxvdTEbMBkGA1UEAwwSSG91
|
||||
dGVzaXBsb3UgQ2xpZW50MCowBQYDK2VwAyEAFkZcOJ5bNr2NZcwZuXkdC5PfX+fu
|
||||
M+YWyh5eeMtU7mujgcUwgcIwCQYDVR0TBAIwADARBglghkgBhvhCAQEEBAMCBaAw
|
||||
MwYJYIZIAYb4QgENBCYWJE9wZW5TU0wgR2VuZXJhdGVkIENsaWVudCBDZXJ0aWZp
|
||||
Y2F0ZTAdBgNVHQ4EFgQUxag2EgTttBUfjSg/MHMs+4h/16cwHwYDVR0jBBgwFoAU
|
||||
4I4VHClHhwpEbdq7impTm1zwi04wDgYDVR0PAQH/BAQDAgXgMB0GA1UdJQQWMBQG
|
||||
CCsGAQUFBwMCBggrBgEFBQcDBDAFBgMrZXADQQAezi5mtJ3Y5A8DLuxL7g9rPff7
|
||||
qUQqwg3RlGQ7QAkwiuUa6nQ1kfcIGSTFe6sOBL5ACMLwxP1ibgAENxBamvAC
|
||||
-----END CERTIFICATE-----
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEICS5j5g1kNSbzhCRT7eDN4k12P0x5n8tg6HLPcCuY7DD
|
||||
-----END PRIVATE KEY-----
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIDFzCCAf+gAwIBAgIUfnEIpcavhz3Pn9P5YMIZzku+OJUwDQYJKoZIhvcNAQEL
|
||||
BQAwGjEYMBYGA1UEAwwPU2VsZlNpZ25lZCBMbXRkMCAXDTI0MDQyMjE1MTQ1N1oY
|
||||
DzMwMjMwODI0MTUxNDU3WjAaMRgwFgYDVQQDDA9TZWxmU2lnbmVkIExtdGQwggEi
|
||||
MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIfgXKzK/FiQzqqAtOCqbTSCtp
|
||||
rFr5hpTQJWscfQXfkQiJC0nLLojj4sLSNf/PnP8eglju+AIO6st2TF3v+qugog0o
|
||||
D9cMvjhzqwLfpJzSr7EN5n4X3TAoyucPg/aF3+ACXwVXuTfxTCbJbEeBrWSk7b1k
|
||||
R1bORVQvH895V08rVs2MZ5v3sutF0EWeiyh4O/EzpSiKywLUE5eLoZiJtN9j3yGm
|
||||
bJii/fCcON4sQTxpL2/i2jbPo2dH2EKKS1xO0LBt4IoiOidRTQiOsjL2VANZiCer
|
||||
UuWUlsG5albCcExvVzQnINRb7+6F6NJqJdC+rRqQG6YeXpUeXBe8GMZjpqLdAgMB
|
||||
AAGjUzBRMB0GA1UdDgQWBBSRC2v9uCKuJ6lpWVbe2nO6gI6bJTAfBgNVHSMEGDAW
|
||||
gBSRC2v9uCKuJ6lpWVbe2nO6gI6bJTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
|
||||
DQEBCwUAA4IBAQDADAKCY4WDhVJnReyP2fc/E/N2Wa29BCdR9LZxS2sNd5gyQnZJ
|
||||
FHLqu1R+tMACtXG4q1AEuN0yn7hsqCfy3hQH5mFLqQzNqoWF6HDQr2b7kfw/E1S3
|
||||
bH/K+6Kbu5fEYWRZNo5Ti62mEHhSS2tBw0qmN9pceT/ZFhYpe44RQzuxEckOXBpx
|
||||
g1J8NAqvknfvl9nu31C8Ye98XlsHDGz/qUvmIPqXCmmg26NcT/NE26A76tgegUDo
|
||||
U/K4WFOr5cyHcIfaYWFg2/SKukudQldwU92ZvJOSwYvn3MKixg/YZGK+0MwzMm4c
|
||||
+ZKttL1Pwub4Gt9ZaFrzrM1t2NRTgMiKU2lQ
|
||||
-----END CERTIFICATE-----
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDIfgXKzK/FiQzq
|
||||
qAtOCqbTSCtprFr5hpTQJWscfQXfkQiJC0nLLojj4sLSNf/PnP8eglju+AIO6st2
|
||||
TF3v+qugog0oD9cMvjhzqwLfpJzSr7EN5n4X3TAoyucPg/aF3+ACXwVXuTfxTCbJ
|
||||
bEeBrWSk7b1kR1bORVQvH895V08rVs2MZ5v3sutF0EWeiyh4O/EzpSiKywLUE5eL
|
||||
oZiJtN9j3yGmbJii/fCcON4sQTxpL2/i2jbPo2dH2EKKS1xO0LBt4IoiOidRTQiO
|
||||
sjL2VANZiCerUuWUlsG5albCcExvVzQnINRb7+6F6NJqJdC+rRqQG6YeXpUeXBe8
|
||||
GMZjpqLdAgMBAAECggEAF197B7eRXZB1dTJjpan1OoKv2PETEVg1/LFYCX6uaBqi
|
||||
1EMrdlM1pId42dkaD5czC1imddlreAIO99AS+sPjgpBRw1QGvda72F7dMLr33fyb
|
||||
F06XGutveVn00j4P8m8f0MxItaot549Oou22GlvfSfY96DUEMtDUmG0iGqMUji3V
|
||||
SeHvAeginTCaoux5OPdB94jcRpsSf+B0pRS0JMpW/87+mg3bzT8ouPYkbfRZ92Wo
|
||||
4x3b1kcBbq7xvtmSsN0PxqZGcuuDn3zjO1XI6HCwWWP+ICUIC6+Tw8szWIvJ9Cgp
|
||||
te2nvcAYstnrW15smJTjRNhikMpu3EsBajsgfsScywKBgQDyoAGlucEfREVmVuba
|
||||
hbLfhGTmKvvsgS2e7p5ktJPkdCyvImU6l9Crzt8qD+9ic2LEgvRBtqaaHkC8DgLc
|
||||
LiEL/QiIbNdGhfxm9+bWMW0wx6HWRwC1b7t1BT3Dh4y9I4YYkLR2NDqh1R8D4X0D
|
||||
mEk9pfR2nFEDjkGyDp9gyCB68wKBgQDTi21DuKu14WQCfrInn3SGoDTeHGRhV6b6
|
||||
HrOJj4ppbqffjUcvvpSzYJAZtk8eHdHMM8tut+OejxPzabFdgXrO4EaMiH5XYaTx
|
||||
zDFpTe/cGZvb5m9jg6fEb0YCB6KfefDpFABXOlaILyl8JwnC0BT1bEL7k8c/s/it
|
||||
LIVIy38+7wKBgQDG0lonPZ5FigO5BpOtFQzs36hzeVvyhjUlXXNNITFkb9NCPVRo
|
||||
/ImSkTcNV/uaWOXiFVImG5BREWOI945ecirAkT0R1udesmOQ2as/cUeCRsWXO54Y
|
||||
EJS0I3Rmq8ioIdk8fjB0AP7fKS9+VaTFcmDqdPlszVISMNwjFpqCi90aAwKBgQCT
|
||||
RkxJi3Wv6DyyJ/Zr820ylLJ5t5aC1n0fQOSJbm9UO3+P+VGIAcyQnTd1TyEBvIzk
|
||||
92I3sLo9FysymXCrwor3H9i92gDrYMVuuVPlFidZOlLx4xnFVFEmRrmcjChBkqmP
|
||||
+ybJk4nOwdbF4n+/KxKMUlTHxPhAd1E3bvlT1qi97QKBgAHUTniLWiqxH4wyspIW
|
||||
SV7EW87Suat+p6TdR81FonNAgLMGjtTqRcHkoCfQfExpPycuybpqtJK7IeiVqNc5
|
||||
lCe48Hl6hpASfwe9T2K3q68UACU5pk5i/PTydFCI+zwVbaiBGip71skzGUVzcPY9
|
||||
JBbw8hEOjPwcK4SAgi+Yprmc
|
||||
-----END PRIVATE KEY-----
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
MIIChzCCAjmgAwIBAgICEAEwBQYDK2VwMFExCzAJBgNVBAYTAkJFMRQwEgYDVQQH
|
||||
DAtIb3V0ZXNpcGxvdTEsMCoGA1UEAwwjSG91dGVzaXBsb3UgQ2VydGlmaWNhdGlv
|
||||
biBBdXRob3JpdHkwIBcNMjQwNDIyMTUxNDU3WhgPMzAyMzA4MjQxNTE0NTdaMEAx
|
||||
CzAJBgNVBAYTAkJFMRQwEgYDVQQHDAtIb3V0ZXNpcGxvdTEbMBkGA1UEAwwSSG91
|
||||
dGVzaXBsb3UgU2VydmVyMCowBQYDK2VwAyEACb6GUUvOz8O1IlrH9/4sxaRGAemi
|
||||
c/eMlQ7xjAjUbLOjggFCMIIBPjAJBgNVHRMEAjAAMBEGCWCGSAGG+EIBAQQEAwIG
|
||||
QDAzBglghkgBhvhCAQ0EJhYkT3BlblNTTCBHZW5lcmF0ZWQgU2VydmVyIENlcnRp
|
||||
ZmljYXRlMB0GA1UdDgQWBBRkIUgQms0wKozvwQp1b1XU53E3kjCBjgYDVR0jBIGG
|
||||
MIGDgBTgjhUcKUeHCkRt2ruKalObXPCLTqFVpFMwUTELMAkGA1UEBhMCQkUxFDAS
|
||||
BgNVBAcMC0hvdXRlc2lwbG91MSwwKgYDVQQDDCNIb3V0ZXNpcGxvdSBDZXJ0aWZp
|
||||
Y2F0aW9uIEF1dGhvcml0eYIUXOt3OuI/y68E6ogXPRKjMxM3d4kwDgYDVR0PAQH/
|
||||
BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMBQGA1UdEQQNMAuCCWxvY2FsaG9z
|
||||
dDAFBgMrZXADQQDidpmoltxDR1Qh5ZDluVC1DENZpXk+My23SDzATvxovbmkHEK2
|
||||
/LdWbqrTPJZIHGhs9W/7vWSbLzyHYTKH2fAD
|
||||
-----END CERTIFICATE-----
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
MC4CAQAwBQYDK2VwBCIEIHSVKSyvMJwmNHpoMj3BLlBy40bQ2vJJyUNknXcyPUkO
|
||||
-----END PRIVATE KEY-----
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import contextlib
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests.common import TransactionCase, tagged
|
||||
|
||||
|
||||
IGNORE_MODEL_NAMES = {
|
||||
'ir.attachment',
|
||||
'test_new_api.attachment',
|
||||
'payment.link.wizard',
|
||||
'account.multicurrency.revaluation.wizard',
|
||||
'account_followup.manual_reminder',
|
||||
}
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestEveryModel(TransactionCase):
|
||||
|
||||
def test_display_name_new_record(self):
|
||||
for model_name in self.registry:
|
||||
model = self.env[model_name]
|
||||
if model._abstract or not model._auto or model_name in IGNORE_MODEL_NAMES:
|
||||
continue
|
||||
|
||||
with self.subTest(
|
||||
msg="`_compute_display_name` doesn't work with new record (first onchange call).",
|
||||
model=model_name,
|
||||
):
|
||||
# Check that the first onchange with display_name works on every models
|
||||
# OR it will fail anyway when people will use click on New
|
||||
fields_used = model._fields['display_name'].get_depends(model)[0]
|
||||
fields_used = [f.split('.', 1)[0] for f in fields_used]
|
||||
fields_spec = dict.fromkeys(fields_used + ['display_name'], {})
|
||||
with contextlib.suppress(UserError):
|
||||
model.onchange({}, [], fields_spec)
|
||||
|
|
@ -0,0 +1,426 @@
|
|||
import contextlib
|
||||
import logging
|
||||
import shutil
|
||||
import smtplib
|
||||
import socket
|
||||
import ssl
|
||||
import unittest
|
||||
import warnings
|
||||
from base64 import b64encode
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
from socket import getaddrinfo # keep a reference on the non-patched function
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import file_path, mute_logger
|
||||
from .common import TransactionCaseWithUserDemo
|
||||
|
||||
try:
|
||||
import aiosmtpd
|
||||
import aiosmtpd.controller
|
||||
import aiosmtpd.smtp
|
||||
import aiosmtpd.handlers
|
||||
except ImportError:
|
||||
aiosmtpd = None
|
||||
|
||||
|
||||
PASSWORD = 'secretpassword'
|
||||
_openssl = shutil.which('openssl')
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _find_free_local_address():
|
||||
""" Get a triple (family, address, port) on which it possible to bind
|
||||
a local tcp service. """
|
||||
addr = aiosmtpd.controller.get_localhost() # it returns 127.0.0.1 or ::1
|
||||
family = socket.AF_INET if addr == '127.0.0.1' else socket.AF_INET6
|
||||
with socket.socket(family, socket.SOCK_STREAM) as sock:
|
||||
sock.bind((addr, 0))
|
||||
port = sock.getsockname()[1]
|
||||
return family, addr, port
|
||||
|
||||
|
||||
def _smtp_authenticate(server, session, enveloppe, mechanism, data):
|
||||
""" Callback method used by aiosmtpd to validate a login/password pair. """
|
||||
result = aiosmtpd.smtp.AuthResult(success=data.password == PASSWORD.encode())
|
||||
_logger.debug("AUTH %s", "successfull" if result.success else "failed")
|
||||
return result
|
||||
|
||||
|
||||
class Certificate:
|
||||
def __init__(self, key, cert):
|
||||
self.key = key and Path(file_path(key, filter_ext='.pem'))
|
||||
self.cert = Path(file_path(cert, filter_ext='.pem'))
|
||||
|
||||
def __repr__(self):
|
||||
return f"Certificate({self.key=}, {self.cert=})"
|
||||
|
||||
|
||||
# skip when optional dependencies are not found
|
||||
@unittest.skipUnless(aiosmtpd, "aiosmtpd couldn't be imported")
|
||||
@unittest.skipUnless(_openssl, "openssl not found in path")
|
||||
# fail fast for timeout errors
|
||||
@patch('odoo.addons.base.models.ir_mail_server.SMTP_TIMEOUT', .1)
|
||||
# prevent the CLI from interfering with the tests
|
||||
@patch.dict('odoo.tools.config.options', {'smtp_server': ''})
|
||||
class TestIrMailServerSMTPD(TransactionCaseWithUserDemo):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
# aiosmtpd emits deprecation warnings because it uses its own
|
||||
# deprecated features, mute those logs.
|
||||
# https://github.com/aio-libs/aiosmtpd/issues/347
|
||||
class Session(aiosmtpd.smtp.Session):
|
||||
@property
|
||||
def login_data(self):
|
||||
return self._login_data
|
||||
@login_data.setter
|
||||
def login_data(self, value):
|
||||
self._login_data = value
|
||||
patcher = patch('aiosmtpd.smtp.Session', Session)
|
||||
patcher.start()
|
||||
cls.addClassCleanup(patcher.stop)
|
||||
|
||||
# aiosmtpd emits warnings for some unusual configuration, like
|
||||
# requiring AUTH on a clear-text transport. Mute those logs
|
||||
# since we also test those unusual configurations.
|
||||
warnings.filterwarnings(
|
||||
'ignore',
|
||||
"Requiring AUTH while not requiring TLS can lead to security vulnerabilities!",
|
||||
category=UserWarning
|
||||
)
|
||||
class CustomFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
if record.msg == "auth_required == True but auth_require_tls == False":
|
||||
return False
|
||||
if record.msg == "tls_context.verify_mode not in {CERT_NONE, CERT_OPTIONAL}; this might cause client connection problems":
|
||||
return False
|
||||
return True
|
||||
logging.getLogger('mail.log').addFilter(CustomFilter())
|
||||
|
||||
# decrease aiosmtpd verbosity, odoo INFO = aiosmtpd WARNING
|
||||
logging.getLogger('mail.log').setLevel(_logger.getEffectiveLevel() + 10)
|
||||
|
||||
# Get various TLS keys and certificates. CA was used to sign
|
||||
# both client and server. self_signed is... self signed.
|
||||
cls.ssl_ca, cls.ssl_client, cls.ssl_server, cls.ssl_self_signed = [
|
||||
Certificate(None, 'base/tests/ssl/ca.cert.pem'),
|
||||
Certificate('base/tests/ssl/client.key.pem',
|
||||
'base/tests/ssl/client.cert.pem'),
|
||||
Certificate('base/tests/ssl/server.key.pem',
|
||||
'base/tests/ssl/server.cert.pem'),
|
||||
Certificate('base/tests/ssl/self_signed.key.pem',
|
||||
'base/tests/ssl/self_signed.cert.pem'),
|
||||
]
|
||||
|
||||
# Patch the two SMTP client classes into trusting the above CA
|
||||
class TEST_SMTP(smtplib.SMTP):
|
||||
def starttls(self, *, context):
|
||||
if context is None:
|
||||
context = ssl._create_stdlib_context() # what SMTP_SSL does
|
||||
# context = ssl.create_default_context() # what it should do
|
||||
context.load_verify_locations(cafile=str(cls.ssl_ca.cert))
|
||||
super().starttls(context=context)
|
||||
class TEST_SMTP_SSL(smtplib.SMTP_SSL):
|
||||
def _get_socket(self, *args, **kwargs):
|
||||
# self.context = ssl.create_default_context() # what it should do
|
||||
self.context.load_verify_locations(cafile=str(cls.ssl_ca.cert))
|
||||
return super()._get_socket(*args, **kwargs)
|
||||
patcher = patch('smtplib.SMTP', TEST_SMTP)
|
||||
patcher.start()
|
||||
cls.addClassCleanup(patcher.stop)
|
||||
patcher = patch('smtplib.SMTP_SSL', TEST_SMTP_SSL)
|
||||
patcher.start()
|
||||
cls.addClassCleanup(patcher.stop)
|
||||
|
||||
# reactivate sending emails during this test suite, make sure
|
||||
# NOT TO send emails using another ir.mail_server than the one
|
||||
# created in setUp!
|
||||
patcher = patch.object(cls.registry['ir.mail_server'], '_is_test_mode')
|
||||
mock = patcher.start()
|
||||
mock.return_value = False
|
||||
cls.addClassCleanup(patcher.stop)
|
||||
|
||||
# fix runbot, docker uses a single ipv4 stack but it gives ::1
|
||||
# when resolving "localhost" (so stupid), use the following to
|
||||
# force aiosmtpd/odoo to bind/connect to a fixed ipv4 OR ipv6
|
||||
# address.
|
||||
family, _, cls.port = _find_free_local_address()
|
||||
cls.localhost = getaddrinfo('localhost', cls.port, family)
|
||||
cls.startClassPatcher(patch('socket.getaddrinfo', cls.getaddrinfo))
|
||||
|
||||
@classmethod
|
||||
def getaddrinfo(cls, host, port, *args, **kwargs):
|
||||
"""
|
||||
Resolve both "localhost" and "notlocalhost" on the ip address
|
||||
bound by aiosmtpd inside `start_smtpd`.
|
||||
"""
|
||||
if host in ('localhost', 'notlocalhost') and port == cls.port:
|
||||
return cls.localhost
|
||||
return getaddrinfo(host, port, family=0, type=0, proto=0, flags=0)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def start_smtpd(
|
||||
self, encryption, ssl_context=None, auth_required=True, stop_on_cleanup=True
|
||||
):
|
||||
"""
|
||||
Start a smtp daemon in a background thread, stop it upon exiting
|
||||
the context manager.
|
||||
|
||||
:param encryption: 'none', 'ssl' or 'starttls', the kind of
|
||||
server to start.
|
||||
:param ssl_context: the ``ssl.SSLContext`` object to use with
|
||||
'ssl' or 'starttls'.
|
||||
:param auth_required: whether the server enforces password
|
||||
authentication or not.
|
||||
"""
|
||||
assert encryption in ('none', 'ssl', 'starttls')
|
||||
assert encryption == 'none' or ssl_context
|
||||
|
||||
kwargs = {}
|
||||
if encryption == 'starttls':
|
||||
# for aiosmtpd.smtp.SMTP
|
||||
kwargs.update({
|
||||
'require_starttls': True,
|
||||
'tls_context': ssl_context,
|
||||
})
|
||||
elif encryption == 'ssl':
|
||||
# for aiosmtpd.controller.InetMixin
|
||||
kwargs['ssl_context'] = ssl_context
|
||||
if auth_required:
|
||||
kwargs['authenticator'] = _smtp_authenticate
|
||||
|
||||
smtpd_thread = aiosmtpd.controller.Controller(
|
||||
aiosmtpd.handlers.Debugging(),
|
||||
hostname=aiosmtpd.controller.get_localhost(),
|
||||
server_hostname='localhost',
|
||||
port=self.port,
|
||||
auth_required=auth_required,
|
||||
auth_require_tls=False,
|
||||
enable_SMTPUTF8=True,
|
||||
**kwargs,
|
||||
)
|
||||
try:
|
||||
smtpd_thread.start()
|
||||
yield smtpd_thread
|
||||
finally:
|
||||
smtpd_thread.stop()
|
||||
|
||||
@mute_logger('mail.log')
|
||||
def test_authentication_certificate_matrix(self):
|
||||
"""
|
||||
Connect to a server that is authenticating users via a TLS
|
||||
certificate. Test the various possible configurations (missing
|
||||
cert, invalid cert and valid cert) against both a STARTTLS and
|
||||
a SSL/TLS SMTP server.
|
||||
"""
|
||||
mail_server = self.env['ir.mail_server'].create({
|
||||
'name': 'test smtpd',
|
||||
'from_filter': 'localhost',
|
||||
'smtp_host': 'localhost',
|
||||
'smtp_port': self.port,
|
||||
'smtp_authentication': 'login',
|
||||
'smtp_user': '',
|
||||
'smtp_pass': '',
|
||||
})
|
||||
|
||||
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
ssl_context.load_cert_chain(self.ssl_server.cert, self.ssl_server.key)
|
||||
ssl_context.load_verify_locations(cafile=self.ssl_ca.cert)
|
||||
ssl_context.verify_mode = ssl.CERT_REQUIRED
|
||||
|
||||
self_signed_key = b64encode(self.ssl_self_signed.key.read_bytes())
|
||||
self_signed_cert = b64encode(self.ssl_self_signed.cert.read_bytes())
|
||||
client_key = b64encode(self.ssl_client.key.read_bytes())
|
||||
client_cert = b64encode(self.ssl_client.cert.read_bytes())
|
||||
matrix = [
|
||||
# authentication, name, certificate, private key, error pattern
|
||||
('login', "missing", '', '',
|
||||
r"The server has closed the connection unexpectedly\. "
|
||||
r"Check configuration served on this port number\.\n "
|
||||
r"Connection unexpectedly closed"),
|
||||
('certificate', "self signed", self_signed_cert, self_signed_key,
|
||||
r"The server has closed the connection unexpectedly\. "
|
||||
r"Check configuration served on this port number\.\n "
|
||||
r"Connection unexpectedly closed"),
|
||||
('certificate', "valid client", client_cert, client_key, None),
|
||||
]
|
||||
|
||||
for encryption in ('starttls', 'ssl'):
|
||||
mail_server.smtp_encryption = encryption
|
||||
with self.start_smtpd(encryption, ssl_context, auth_required=False):
|
||||
for authentication, name, certificate, private_key, error_pattern in matrix:
|
||||
with self.subTest(encryption=encryption, certificate=name):
|
||||
mail_server.write({
|
||||
'smtp_authentication': authentication,
|
||||
'smtp_ssl_certificate': certificate,
|
||||
'smtp_ssl_private_key': private_key,
|
||||
})
|
||||
if error_pattern:
|
||||
with self.assertRaises(UserError) as error_capture:
|
||||
mail_server.test_smtp_connection()
|
||||
self.assertRegex(error_capture.exception.args[0], error_pattern)
|
||||
else:
|
||||
mail_server.test_smtp_connection()
|
||||
|
||||
|
||||
def test_authentication_login_matrix(self):
|
||||
"""
|
||||
Connect to a server that is authenticating users via a login/pwd
|
||||
pair. Test the various possible configurations (missing pair,
|
||||
invalid pair and valid pair) against both a SMTP server without
|
||||
encryption, a STARTTLS and a SSL/TLS SMTP server.
|
||||
"""
|
||||
mail_server = self.env['ir.mail_server'].create({
|
||||
'name': 'test smtpd',
|
||||
'from_filter': 'localhost',
|
||||
'smtp_host': 'localhost',
|
||||
'smtp_port': self.port,
|
||||
'smtp_authentication': 'login',
|
||||
'smtp_user': '',
|
||||
'smtp_pass': '',
|
||||
})
|
||||
|
||||
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
ssl_context.load_cert_chain(self.ssl_server.cert, self.ssl_server.key)
|
||||
|
||||
MISSING = ''
|
||||
INVALID = 'bad password'
|
||||
matrix = [
|
||||
# auth_required, password, error_pattern
|
||||
(False, MISSING, None),
|
||||
(True, MISSING,
|
||||
r"The server refused the sender address \(noreply@localhost\) "
|
||||
r"with error b'5\.7\.0 Authentication required'"),
|
||||
(True, INVALID,
|
||||
r"The server has closed the connection unexpectedly\. "
|
||||
r"Check configuration served on this port number\.\n "
|
||||
r"Connection unexpectedly closed:.* timed out"),
|
||||
(True, PASSWORD, None),
|
||||
]
|
||||
|
||||
for encryption in ('none', 'starttls', 'ssl'):
|
||||
mail_server.smtp_encryption = encryption
|
||||
for auth_required, password, error_pattern in matrix:
|
||||
mail_server.smtp_user = password and self.user_demo.email
|
||||
mail_server.smtp_pass = password
|
||||
with self.subTest(encryption=encryption,
|
||||
auth_required=auth_required,
|
||||
password=password):
|
||||
with self.start_smtpd(encryption, ssl_context, auth_required):
|
||||
if error_pattern:
|
||||
with self.assertRaises(UserError) as capture:
|
||||
mail_server.test_smtp_connection()
|
||||
self.assertRegex(capture.exception.args[0], error_pattern)
|
||||
else:
|
||||
mail_server.test_smtp_connection()
|
||||
|
||||
@mute_logger('mail.log')
|
||||
def test_encryption_matrix(self):
|
||||
"""
|
||||
Connect to a server on a different encryption configuration than
|
||||
the server is configured. Verify that it crashes with a good
|
||||
error message.
|
||||
"""
|
||||
mail_server = self.env['ir.mail_server'].create({
|
||||
'name': 'test smtpd',
|
||||
'from_filter': 'localhost',
|
||||
'smtp_host': 'localhost',
|
||||
'smtp_port': self.port,
|
||||
'smtp_authentication': 'login',
|
||||
'smtp_user': '',
|
||||
'smtp_pass': '',
|
||||
})
|
||||
|
||||
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
ssl_context.load_cert_chain(self.ssl_server.cert, self.ssl_server.key)
|
||||
|
||||
matrix = [
|
||||
# client, server, error_pattern
|
||||
('none', 'ssl',
|
||||
r"The server has closed the connection unexpectedly\. "
|
||||
r"Check configuration served on this port number\.\n "
|
||||
r"Connection unexpectedly closed: timed out"),
|
||||
('none', 'starttls',
|
||||
r"The server refused the sender address \(noreply@localhost\) with error "
|
||||
r"b'Must issue a STARTTLS command first'"),
|
||||
('starttls', 'none',
|
||||
r"An option is not supported by the server:\n "
|
||||
r"STARTTLS extension not supported by server\."),
|
||||
('starttls', 'ssl',
|
||||
r"The server has closed the connection unexpectedly\. "
|
||||
r"Check configuration served on this port number\.\n "
|
||||
r"Connection unexpectedly closed: timed out"),
|
||||
('ssl', 'none',
|
||||
r"An SSL exception occurred\. "
|
||||
r"Check connection security type\.\n "
|
||||
r".*?wrong version number"),
|
||||
('ssl', 'starttls',
|
||||
r"An SSL exception occurred\. "
|
||||
r"Check connection security type\.\n "
|
||||
r".*?wrong version number"),
|
||||
]
|
||||
|
||||
for client_encryption, server_encryption, error_pattern in matrix:
|
||||
with self.subTest(server_encryption=server_encryption,
|
||||
client_encryption=client_encryption):
|
||||
mail_server.smtp_encryption = client_encryption
|
||||
with self.start_smtpd(server_encryption, ssl_context, auth_required=False):
|
||||
with self.assertRaises(UserError) as capture:
|
||||
mail_server.test_smtp_connection()
|
||||
self.assertRegex(capture.exception.args[0], error_pattern)
|
||||
|
||||
def test_man_in_the_middle_matrix(self):
|
||||
"""
|
||||
Simulate that a pirate was successful at intercepting the live
|
||||
traffic in between the Odoo server and the legitimate SMTP
|
||||
server.
|
||||
"""
|
||||
mail_server = self.env['ir.mail_server'].create({
|
||||
'name': 'test smtpd',
|
||||
'from_filter': 'localhost',
|
||||
'smtp_host': 'localhost',
|
||||
'smtp_port': self.port,
|
||||
'smtp_authentication': 'login',
|
||||
'smtp_user': self.user_demo.email,
|
||||
'smtp_pass': PASSWORD,
|
||||
'smtp_ssl_certificate': b64encode(self.ssl_client.cert.read_bytes()),
|
||||
'smtp_ssl_private_key': b64encode(self.ssl_client.key.read_bytes()),
|
||||
})
|
||||
|
||||
cert_good = self.ssl_server
|
||||
cert_bad = self.ssl_self_signed
|
||||
host_good = 'localhost'
|
||||
host_bad = 'notlocalhost'
|
||||
|
||||
# for now it doesn't raise any error for bad cert/host
|
||||
matrix = [
|
||||
# authentication, certificate, hostname, error_pattern
|
||||
('login', cert_bad, host_good, None),
|
||||
('login', cert_good, host_bad, None),
|
||||
('certificate', cert_bad, host_good, None),
|
||||
('certificate', cert_good, host_bad, None),
|
||||
]
|
||||
|
||||
for encryption in ('starttls', 'ssl'):
|
||||
for authentication, certificate, hostname, error_pattern in matrix:
|
||||
mail_server.smtp_host = hostname
|
||||
mail_server.smtp_authentication = authentication
|
||||
mail_server.smtp_encryption = encryption
|
||||
with self.subTest(
|
||||
encryption=encryption,
|
||||
authentication=authentication,
|
||||
cert_good=certificate == cert_good,
|
||||
host_good=hostname == host_good,
|
||||
):
|
||||
mitm_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
mitm_context.load_cert_chain(certificate.cert, certificate.key)
|
||||
auth_required = authentication == 'login'
|
||||
with self.start_smtpd(encryption, mitm_context, auth_required):
|
||||
if error_pattern:
|
||||
with self.assertRaises(UserError) as capture:
|
||||
mail_server.test_smtp_connection()
|
||||
self.assertRegex(capture.exception.args[0], error_pattern)
|
||||
else:
|
||||
mail_server.test_smtp_connection()
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tests import TransactionCase, tagged
|
||||
|
||||
|
||||
@tagged('-at_install', 'post_install')
|
||||
class TestOverrides(TransactionCase):
|
||||
|
||||
# Ensure all main ORM methods behavior works fine even on empty recordset
|
||||
# and that their returned value(s) follow the expected format.
|
||||
|
||||
def test_creates(self):
|
||||
for model_env in self.env.values():
|
||||
if model_env._abstract:
|
||||
continue
|
||||
# with self.assertQueryCount(0):
|
||||
self.assertEqual(
|
||||
model_env.create([]), model_env.browse(),
|
||||
"Invalid create return value for model %s" % model_env._name)
|
||||
|
||||
def test_writes(self):
|
||||
for model_env in self.env.values():
|
||||
if model_env._abstract:
|
||||
continue
|
||||
try:
|
||||
# with self.assertQueryCount(0):
|
||||
self.assertEqual(
|
||||
model_env.browse().write({}), True,
|
||||
"Invalid write return value for model %s" % model_env._name)
|
||||
except UserError:
|
||||
# skip models that should never be modified
|
||||
continue
|
||||
|
||||
def test_default_get(self):
|
||||
for model_env in self.env.values():
|
||||
if model_env._transient:
|
||||
continue
|
||||
try:
|
||||
# with self.assertQueryCount(1): # allow one query for the call to get_model_defaults.
|
||||
self.assertEqual(
|
||||
model_env.browse().default_get([]), {},
|
||||
"Invalid default_get return value for model %s" % model_env._name)
|
||||
except UserError:
|
||||
# skip "You must be logged in a Belgian company to use this feature" errors
|
||||
continue
|
||||
|
||||
def test_unlink(self):
|
||||
for model_env in self.env.values():
|
||||
if model_env._abstract:
|
||||
continue
|
||||
# with self.assertQueryCount(0):
|
||||
self.assertEqual(
|
||||
model_env.browse().unlink(), True,
|
||||
"Invalid unlink return value for model %s" % model_env._name)
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
from odoo.tests.common import TransactionCase
|
||||
from odoo.tools import file_open
|
||||
from odoo.addons.base.models.ir_actions_report import _split_table
|
||||
from lxml import etree
|
||||
|
||||
def cleanup_string(s):
|
||||
return ''.join(s.split())
|
||||
|
||||
class TestSplitTable(TransactionCase):
|
||||
def test_split_table(self):
|
||||
# NOTE: All the tests's xml are in split_table/ relative to this file
|
||||
CASES = (
|
||||
("Table's len is equal to max_rows and should not be split", "simple", "simple", 3),
|
||||
("Table's len is greater to max_rows and should not be split", "simple", "simple", 4),
|
||||
("max_rows is 1 and every table should be split", "simple", "simple.split1", 1),
|
||||
("max_row is 2 and the table should be split", "simple", "simple.split2", 2),
|
||||
("Nested tables should be split", "nested", "nested.split2", 2),
|
||||
("Nested tables at the start should be split", "first_nested", "first_nested.split2", 2),
|
||||
("Attributes should be copied", "copy_attributes", "copy_attributes.split1", 1),
|
||||
)
|
||||
|
||||
for description, actual, expected, max_rows in CASES:
|
||||
with self.subTest(description), \
|
||||
file_open(f"base/tests/split_table/{actual}.xml") as actual, \
|
||||
file_open(f"base/tests/split_table/{expected}.xml") as expected:
|
||||
|
||||
tree = etree.fromstring(actual.read())
|
||||
_split_table(tree, max_rows)
|
||||
processed = etree.tostring(tree, encoding='unicode')
|
||||
self.assertEqual(cleanup_string(processed), cleanup_string(expected.read()))
|
||||
177
odoo-bringout-oca-ocb-base/odoo/addons/base/tests/test_sql.py
Normal file
177
odoo-bringout-oca-ocb-base/odoo/addons/base/tests/test_sql.py
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from psycopg2.errors import CheckViolation
|
||||
|
||||
from odoo.tests.common import BaseCase, TransactionCase
|
||||
from odoo.tools import SQL, mute_logger, sql
|
||||
|
||||
|
||||
class TestSQL(BaseCase):
|
||||
|
||||
def test_sql_empty(self):
|
||||
sql = SQL()
|
||||
self.assertEqual(sql.code, "")
|
||||
self.assertEqual(sql.params, [])
|
||||
|
||||
def test_sql_bool(self):
|
||||
self.assertFalse(SQL())
|
||||
self.assertFalse(SQL(""))
|
||||
self.assertTrue(SQL("anything"))
|
||||
self.assertTrue(SQL("%s", 42))
|
||||
|
||||
def test_sql_with_no_parameter(self):
|
||||
sql = SQL("SELECT id FROM table WHERE foo=bar")
|
||||
self.assertEqual(sql.code, "SELECT id FROM table WHERE foo=bar")
|
||||
self.assertEqual(sql.params, [])
|
||||
|
||||
def test_sql_with_literal_parameters(self):
|
||||
sql = SQL("SELECT id FROM table WHERE foo=%s AND bar=%s", 42, 'baz')
|
||||
self.assertEqual(sql.code, "SELECT id FROM table WHERE foo=%s AND bar=%s")
|
||||
self.assertEqual(sql.params, [42, 'baz'])
|
||||
|
||||
def test_sql_with_wrong_pattern(self):
|
||||
with self.assertRaises(TypeError):
|
||||
SQL("SELECT id FROM table WHERE foo=%s AND bar=%s", 42)
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
SQL("SELECT id FROM table WHERE foo=%s AND bar=%s", 1, 2, 3)
|
||||
|
||||
with self.assertRaises(TypeError):
|
||||
SQL("SELECT id FROM table WHERE foo=%s AND bar=%(two)s", 1, two=2)
|
||||
|
||||
with self.assertRaises(KeyError):
|
||||
SQL("SELECT id FROM table WHERE foo=%(one)s AND bar=%(two)s", one=1, to=2)
|
||||
|
||||
def test_sql_equality(self):
|
||||
sql1 = SQL("SELECT id FROM table WHERE foo=%s", 42)
|
||||
sql2 = SQL("SELECT id FROM table WHERE foo=%s", 42)
|
||||
self.assertEqual(sql1, sql2)
|
||||
|
||||
sql1 = SQL("SELECT id FROM table WHERE foo=%s", 42)
|
||||
sql2 = SQL("SELECT id FROM table WHERE bar=%s", 42)
|
||||
self.assertNotEqual(sql1, sql2)
|
||||
|
||||
sql1 = SQL("SELECT id FROM table WHERE foo=%s", 42)
|
||||
sql2 = SQL("SELECT id FROM table WHERE foo=%s", 421)
|
||||
self.assertNotEqual(sql1, sql2)
|
||||
|
||||
def test_sql_idempotence(self):
|
||||
sql1 = SQL("SELECT id FROM table WHERE foo=%s AND bar=%s", 42, 'baz')
|
||||
sql2 = SQL(sql1)
|
||||
self.assertIs(sql1, sql2)
|
||||
|
||||
def test_sql_unpacking(self):
|
||||
sql = SQL("SELECT id FROM table WHERE foo=%s AND bar=%s", 42, 'baz')
|
||||
string, params = sql
|
||||
self.assertEqual(string, "SELECT id FROM table WHERE foo=%s AND bar=%s")
|
||||
self.assertEqual(params, [42, 'baz'])
|
||||
|
||||
def test_sql_join(self):
|
||||
sql = SQL(" AND ").join([])
|
||||
self.assertEqual(sql.code, "")
|
||||
self.assertEqual(sql.params, [])
|
||||
self.assertEqual(sql, SQL(""))
|
||||
|
||||
sql = SQL(" AND ").join([SQL("foo=%s", 1)])
|
||||
self.assertEqual(sql.code, "foo=%s")
|
||||
self.assertEqual(sql.params, [1])
|
||||
|
||||
sql = SQL(" AND ").join([
|
||||
SQL("foo=%s", 1),
|
||||
SQL("bar=%s", 2),
|
||||
SQL("baz=%s", 3),
|
||||
])
|
||||
self.assertEqual(sql.code, "foo=%s AND bar=%s AND baz=%s")
|
||||
self.assertEqual(sql.params, [1, 2, 3])
|
||||
|
||||
sql = SQL(", ").join([1, 2, 3])
|
||||
self.assertEqual(sql.code, "%s, %s, %s")
|
||||
self.assertEqual(sql.params, [1, 2, 3])
|
||||
|
||||
def test_sql_identifier(self):
|
||||
sql = SQL.identifier('foo')
|
||||
self.assertEqual(sql.code, '"foo"')
|
||||
self.assertEqual(sql.params, [])
|
||||
|
||||
sql = SQL.identifier('année')
|
||||
self.assertEqual(sql.code, '"année"')
|
||||
self.assertEqual(sql.params, [])
|
||||
|
||||
sql = SQL.identifier('foo', 'bar')
|
||||
self.assertEqual(sql.code, '"foo"."bar"')
|
||||
self.assertEqual(sql.params, [])
|
||||
|
||||
with self.assertRaises(AssertionError):
|
||||
sql = SQL.identifier('foo"')
|
||||
|
||||
with self.assertRaises(AssertionError):
|
||||
sql = SQL.identifier('(SELECT 42)')
|
||||
|
||||
with self.assertRaises(AssertionError):
|
||||
sql = SQL.identifier('foo', 'ba"r')
|
||||
|
||||
def test_sql_with_sql_parameters(self):
|
||||
sql = SQL("SELECT id FROM table WHERE foo=%s AND %s", 1, SQL("bar=%s", 2))
|
||||
self.assertEqual(sql.code, "SELECT id FROM table WHERE foo=%s AND bar=%s")
|
||||
self.assertEqual(sql.params, [1, 2])
|
||||
self.assertEqual(sql, SQL("SELECT id FROM table WHERE foo=%s AND bar=%s", 1, 2))
|
||||
|
||||
sql = SQL("SELECT id FROM table WHERE %s AND bar=%s", SQL("foo=%s", 1), 2)
|
||||
self.assertEqual(sql.code, "SELECT id FROM table WHERE foo=%s AND bar=%s")
|
||||
self.assertEqual(sql.params, [1, 2])
|
||||
self.assertEqual(sql, SQL("SELECT id FROM table WHERE foo=%s AND bar=%s", 1, 2))
|
||||
|
||||
sql = SQL("SELECT id FROM table WHERE %s AND %s", SQL("foo=%s", 1), SQL("bar=%s", 2))
|
||||
self.assertEqual(sql.code, "SELECT id FROM table WHERE foo=%s AND bar=%s")
|
||||
self.assertEqual(sql.params, [1, 2])
|
||||
self.assertEqual(sql, SQL("SELECT id FROM table WHERE foo=%s AND bar=%s", 1, 2))
|
||||
|
||||
def test_sql_with_named_parameters(self):
|
||||
sql = SQL("SELECT id FROM table WHERE %(one)s AND bar=%(two)s", one=SQL("foo=%s", 1), two=2)
|
||||
self.assertEqual(sql.code, "SELECT id FROM table WHERE foo=%s AND bar=%s")
|
||||
self.assertEqual(sql.params, [1, 2])
|
||||
self.assertEqual(sql, SQL("SELECT id FROM table WHERE foo=%s AND bar=%s", 1, 2))
|
||||
|
||||
# the parameters are bound locally
|
||||
sql = SQL(
|
||||
"%s AND %s",
|
||||
SQL("foo=%(value)s", value=1),
|
||||
SQL("bar=%(value)s", value=2),
|
||||
)
|
||||
self.assertEqual(sql.code, "foo=%s AND bar=%s")
|
||||
self.assertEqual(sql.params, [1, 2])
|
||||
self.assertEqual(sql, SQL("foo=%s AND bar=%s", 1, 2))
|
||||
|
||||
def test_complex_sql(self):
|
||||
sql = SQL(
|
||||
"SELECT %s FROM %s WHERE %s",
|
||||
SQL.identifier('id'),
|
||||
SQL.identifier('table'),
|
||||
SQL(" AND ").join([
|
||||
SQL("%s=%s", SQL.identifier('table', 'foo'), 1),
|
||||
SQL("%s=%s", SQL.identifier('table', 'bar'), 2),
|
||||
]),
|
||||
)
|
||||
self.assertEqual(sql.code, 'SELECT "id" FROM "table" WHERE "table"."foo"=%s AND "table"."bar"=%s')
|
||||
self.assertEqual(sql.params, [1, 2])
|
||||
self.assertEqual(sql, SQL('SELECT "id" FROM "table" WHERE "table"."foo"=%s AND "table"."bar"=%s', 1, 2))
|
||||
self.assertEqual(
|
||||
repr(sql),
|
||||
"""SQL('SELECT "id" FROM "table" WHERE "table"."foo"=%s AND "table"."bar"=%s', 1, 2)"""
|
||||
)
|
||||
|
||||
class TestSqlTools(TransactionCase):
|
||||
|
||||
def test_add_constraint(self):
|
||||
definition = "CHECK (name !~ '%')"
|
||||
sql.add_constraint(self.env.cr, 'res_bank', 'test_constraint_dummy', definition)
|
||||
|
||||
# ensure the constraint with % works and it's in the DB
|
||||
with self.assertRaises(CheckViolation), mute_logger('odoo.sql_db'):
|
||||
self.env['res.bank'].create({'name': '10% bank'})
|
||||
|
||||
# ensure the definitions match
|
||||
db_definition = sql.constraint_definition(self.env.cr, 'res_bank', 'test_constraint_dummy')
|
||||
self.assertEqual(definition, db_definition)
|
||||
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
class TestTransactionEnvs(TransactionCase):
|
||||
def test_transation_envs_weakrefs(self):
|
||||
transaction = self.env.transaction
|
||||
starting_envs = set(transaction.envs)
|
||||
base_x = self.env['base'].with_context(test_stuff=False)
|
||||
self.assertIn(base_x.env, transaction.envs)
|
||||
del base_x
|
||||
self.assertEqual(set(transaction.envs), starting_envs)
|
||||
|
||||
def do_stuff_with_env(self):
|
||||
base_test = self.env['base'].with_context(test_stuff=False)
|
||||
base_test |= self.env['base'].with_context(test_stuff=1)
|
||||
base_test |= self.env['base'].with_context(test_stuff=2)
|
||||
return base_test
|
||||
|
||||
def test_transation_envs_weakrefs_call(self):
|
||||
transaction = self.env.transaction
|
||||
starting_envs = set(transaction.envs)
|
||||
self.do_stuff_with_env()
|
||||
self.assertEqual(set(transaction.envs), starting_envs)
|
||||
|
||||
def test_transation_envs_weakrefs_return(self):
|
||||
transaction = self.env.transaction
|
||||
starting_envs = set(transaction.envs)
|
||||
base_test = self.do_stuff_with_env()
|
||||
self.assertEqual(set(transaction.envs), starting_envs | {base_test.env})
|
||||
|
||||
def test_transation_envs_ordered(self):
|
||||
transaction = self.env.transaction
|
||||
starting_envs = set(transaction.envs)
|
||||
# create environments in a certain order, not sorted on item
|
||||
items = [3, 8, 1, 5, 2, 7, 6, 9, 0, 4]
|
||||
envs = [self.env(context={'item': item}) for item in items]
|
||||
# check that those environments appear in order in transaction.envs
|
||||
env_items = [env.context['item'] for env in transaction.envs if env not in starting_envs]
|
||||
self.assertEqual(env_items, items)
|
||||
del envs
|
||||
self.assertEqual(set(transaction.envs), starting_envs)
|
||||
Loading…
Add table
Add a link
Reference in a new issue